package edu.uw.cs.cse461.sp12.OS;

import java.io.IOException;
import java.util.HashMap;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import edu.uw.cs.cse461.sp12.OS.HTTPDService.HTTPProvider;


public class DDNSService extends RPCCallable implements HTTPProvider {
	
	// A variable capable of describing a method that can be invoked by RPC.
	private RPCCallableMethod<DDNSService> register;
	private RPCCallableMethod<DDNSService> unregister;
	private RPCCallableMethod<DDNSService> resolve;
	Record soa;
	String soaName;
	HashMap<String, String> cnames;
	HashMap<String, Record> records;
	HashMap<String, String> passwords;
	private int lifetime;
	
	
	
	DDNSService() throws Exception {
		// Set up the method descriptor variable to refer to this->_DDNS()
		register = new RPCCallableMethod<DDNSService>(this, "_register");
		resolve = new RPCCallableMethod<DDNSService>(this, "_resolve");
		unregister = new RPCCallableMethod<DDNSService>(this, "_unregister");
		// Register the method with the RPC service as externally invocable method "register", "unregister" and "resolve"
		((RPCService)OS.getRPCService("rpc")).registerHandler(servicename(), "register", register );
		((RPCService)OS.getRPCService("rpc")).registerHandler(servicename(), "unregister", unregister );
		((RPCService)OS.getRPCService("rpc")).registerHandler(servicename(), "resolve", resolve );
		lifetime = Integer.parseInt(OS.config().getProperty("ddns.lifetime"));
		
		
		//setup hashmaps
		cnames = new HashMap<String, String>();
		records = new HashMap<String, Record>();
		passwords = new HashMap<String, String>();
		//System.out.println(lifetime);
		setupRecords();
	} 

	@Override
	public String servicename() {
		return "ddns";
	}

	@Override
	public void shutdown() {
		// TODO Auto-generated method stub
		
	}
	
	
	private void setupRecords(){
		//create a records
		String property = OS.config().getProperty("ddns.a");
		String[] list;
		if (property != null) {
			list = property.split(",");
			addToMap(list, Record.Type.A);
		}
		//create ns records
		property = OS.config().getProperty("ddns.ns");
		if (property != null) {
			list = property.split(",");
			addToMap(list, Record.Type.NS);
		}
		//create SOA record
		property = OS.config().getProperty("ddns.soa");
		list = property.split(",");
		String[] data = list[0].split(":");
		Record r = new Record(null, 0, 0, Record.Type.SOA);
		soa = r;
		soaName = data[0];
		passwords.put(data[0], data[1]);
		
		//create cname records
		property = OS.config().getProperty("ddns.cname");
		if (property != null) {
			list = property.split(",");
			for (String record : list) {
				data = record.split(":");
				cnames.put(data[0], data[1]);
			}
		}
		
	}
	
	private void addToMap(String[] recordsToAdd, Record.Type t) {
		for (String record : recordsToAdd) {
			String[] data = record.split(":");
			Record r = new Record(null, 0, 0, t);
			records.put(data[0], r);
			passwords.put(data[0], data[1]);
		}
	}
	
	
	
	/**
	 * This method is callable by RPC (because of the actions taken by the constructor).
	 * <p>
	 * All RPC-callable methods take a JSONObject as their single parameter, and return
	 * a JSONObject.  (The return value can be null.)  This particular method simply
	 * echos its arguments back to the caller. 
	 * @param args
	 * @return
	 * @throws JSONException
	 */
	public JSONObject _resolve(JSONObject args) throws JSONException, IOException {
		// We can't assume the underlying implementation won't modify args in some way that is
		// incompatible with return value, so have to make a copy of the args.
		
		// ANDROID INCOMPATIBILITY
		//JSONObject result = new JSONObject(args, JSONObject.getNames(args));
		JSONObject result = new JSONObject();
		String name = args.getString("name");
		if (!name.endsWith(soaName)) {
			System.out.println("name: " + name + ", soaName: " + soaName);
			return createException(6, "Incorrect Zone", name, soaName);
		}
		Record r;
		String originalName = name;
		while (!name.equals("") && name.length() >= soaName.length()){
			r = getRecord(name);
			if (r != null) {
					//it's an A or SOA record
					if (name.equals(originalName) && (r.t == Record.Type.A || r.t == Record.Type.SOA)) {
						//it contains a valid address
						if (r.ip != null && r.timeout > System.currentTimeMillis()) {
							result = createTuple(createJSONRecord(name, r), true, "resolveresult");
							return result;
						} else {
							return createException(2, "No address exists yet", name, null);
						}
					//NS record. doesnt need to match full name
					} else if (r.t == Record.Type.NS) {
						if (r.ip != null && r.timeout > System.currentTimeMillis()) {
							result = createTuple(createJSONRecord(name, r), false, "resolveresult");
							return result;
						} else {
							return createException(2, "No address exists yet", name, null);
						}
					}
			} else {
				//check cnames
				String cname = null;
				synchronized(this) {
					if (cnames.containsKey(name)) {
						cname = cnames.get(name);
					}
				}
				if (cname != null) {
					JSONObject record = createCnameRecord(name, cnames.get(name));
					result = createTuple(record, false, "resolveresult");
					return result;
					
				}
			}
			
			String[] s = name.split(".", 2);
			if (s.length == 2) {
				name = s[1];
			}
			else {
				break;
			}
			
		}
		
		return createException(1, "This name does not exist", name, null);
	}
	
	public JSONObject _register(JSONObject args) throws JSONException, IOException {
	
		//System.out.println("register called");
		JSONObject result = new JSONObject();
		String name = args.getString("name");
		String password = args.getString("password");
		int port = args.getInt("port");
		String ip = args.getString("ip");
		Record r;
		String originalName = name;
		while (!name.equals("") && name.length() >= soaName.length()){
			//System.out.println("name checked" + name);
			r = getRecord(name);
			if (r != null) {
					//it's an A or SOA or NS record
					//System.out.println("record found" + name);
					if ((name.equals(originalName) && (r.t == Record.Type.A || r.t == Record.Type.SOA)) || r.t == Record.Type.NS) {
						
						if (name.equals(originalName)) {
							if (passwords.get(name).equals(password)) {
								//it contains a valid address and password is correct, update record
								r.ip = ip;
								r.port = port;
								r.timeout = System.currentTimeMillis() + lifetime * 1000;
								//System.out.println("updated at " + r.timeout);
								synchronized(this) {
									if (r.t == Record.Type.A || r.t == Record.Type.NS) {
										records.put(name, r);
									} else {
										soa = r;
									}
								}
								if (r.ip != null && r.timeout > System.currentTimeMillis()) {
									//if record type is NS, set done to false else true
									result = createTupleWithTimeout(createJSONRecord(name, r), r.t != Record.Type.NS, "registerresult", lifetime);
									return result;
								}
								
							} else {
								return createException(3, "Incorrect Password", name, null);
							}
						} else {
							return createTupleWithTimeout(createJSONRecord(name, r), false, "registerresult", lifetime);
						}
					}
					//does not contain valid address. will happen if we run into an A or cname 
					return createException(1, "This name does not exist", name, null);
			} else {
				//check cnames
				String cname = null;
				synchronized(this) {
					if (cnames.containsKey(name)) {
						cname = cnames.get(name);
					}
				}
				if (cname != null) {
					JSONObject record = createCnameRecord(name, cnames.get(name));
					result = createTupleWithTimeout(record, false, "registerresult", lifetime);
					return result;
					
				}
			}
			String[] s = name.split(".", 2);
			if (s.length == 2) {
				name = s[1];
			}
			else {
				name = "";
			}
		}
		return createException(1, "This name does not exist", name, null);
	}
	
	public JSONObject _unregister(JSONObject args) throws JSONException, IOException {
		
		JSONObject result = new JSONObject();
		String name = args.getString("name");
		String password = args.getString("password");
		Record r;
		String originalName = name;
		//while the name is still longer than our host name, keep checking names. otherwise we won't match anything so exit loop
		while (!name.equals("") && name.length() >= soaName.length()){
			r = getRecord(name);
			//it's an A or SOA or NS record
			if ((name.equals(originalName) && (r.t == Record.Type.A || r.t == Record.Type.SOA)) || r.t == Record.Type.NS) {
				if (name.equals(originalName)) {
					if (passwords.get(name).equals(password)) {
						//it contains a valid address and password is correct, unregister
						r.timeout = 0;
						r.port = 0;
						r.ip = null;
						//if record type is NS, set done to false else true
	                    if (r.t == Record.Type.NS) {
	                            result = createTuple(createJSONRecord(name, r), false, "unregisterresult");
	                    } else {
	                            result = createTuple(null, true, "unregisterresult");
	                    }
						return result;
					} else {
						//incorrect password. don't change anything
						return createException(3, "Incorrect Password", name, null);
					}
				}
				else {
					return createTuple(createJSONRecord(name, r), false, "registerresult");
				}
					
			} else {
				//check cnames
				String cname = null;
				synchronized(this) {
					if (cnames.containsKey(name)) {
						cname = cnames.get(name);
					}
				}
				if (cname != null) {
					JSONObject record = createCnameRecord(name, cnames.get(name));
					result = createTuple(record, false, "unregisterresult");
					return result;
					
				}
			}
			String[] s = name.split(".", 2);
			if (s.length == 2) {
				name = s[1];
			}
			else {
				name = "";
			}
		} 
		System.out.println("unregistering name: " + name);
		return createException(1, "This name does not exist", name, null);
	}
	

	@Override
	public String httpServe(String[] uriArray) {
		// TODO Auto-generated method stub
		//return "DDNSService";
		checkRecordTime(soa);
		String results = "SOA: " + soaName + " " + soa.ip + ":" + soa.port + "\n";
		synchronized(this) {
			results = results + "records\n";
			for (String r : records.keySet()) {
				Record record = records.get(r);
				if (record.timeout > 0) checkRecordTime(record);
				results = results + r + " " + records.get(r).ip + ":" + records.get(r).port + " type:" + record.t + "\n";
			}
		}
		return results;
	}
	
	private Record getRecord(String name) {
		Record r = null;
		synchronized(this) {
			if (records.containsKey(name)) {
				r = records.get(name);
			} else if (soaName.equals(name)){
				r = soa;
			}
		}
		return r;
	}
	
	private Record checkRecordTime(Record record) {
		if (record.timeout > 0 && record.timeout < System.currentTimeMillis()) {
			System.out.println("timeout reached at ");
			record.ip = "";
			record.port = 0;
			record.timeout = 0;
		}
		return record;
	}
	
	private JSONObject createCnameRecord(String name, String alias) throws JSONException {
		JSONObject result = new JSONObject();
		result.put("name", name);
		result.put("type", "CNAME");
		result.put("alias", alias);
		return result;
	}
	
	private JSONObject createJSONRecord(String name, Record r) throws JSONException {
		JSONObject result = new JSONObject();
		result.put("name", name);
		result.put("type", r.t.toString());
		result.put("ip", r.ip);
		result.put("port", r.port);
		return result;
	}
	
	private JSONObject createTuple(JSONObject record, boolean done, String type) throws JSONException {
		JSONObject result = new JSONObject();
		result.put("node", record);
		result.put("done", done);
		result.put("resulttype", type);
		return result;
	}
	
	private JSONObject createTupleWithTimeout(JSONObject record, boolean done, String type, int lifetime) throws JSONException {
		JSONObject result = createTuple(record, done, type);
		result.put("lifetime", lifetime);
		return result;
	}
	
	private JSONObject createException(int exceptionnum, String message, String name, String zone) throws JSONException {
		JSONObject result = new JSONObject();
		result.put("resulttype", "ddnsexception");
		result.put("exceptionnum", exceptionnum);
		result.put("message", message);
		if(name != null) {
			result.put("name", name);
		}
		if(zone!=null) {
			result.put("zone", zone);
		}
		return result;
	}
	

}
